import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jdom.Attribute;
import org.jdom.DataConversionException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

@WebServlet(name = "PeaceShop", urlPatterns = "/PeaceShop", asyncSupported = true)
public class PeaceShop extends HttpServlet {

	private Document items;
	private Queue<Element> latestReviews = new ConcurrentLinkedQueue<Element>();
	private Thread notifierThread = null;

	private static final Queue<AsyncContext> clients = new ConcurrentLinkedQueue<AsyncContext>();
	private static final BlockingQueue<Element> newReviews = new LinkedBlockingQueue<Element>();
	private static final XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
	private static final long serialVersionUID = 1L;
	private static String dbPath = "WEB-INF/productlist.xml";

	/**
	 * @see HttpServlet#HttpServlet()
	 */
	public void init(ServletConfig config) {
		try {
			ServletContext context = config.getServletContext();
			String path = context.getRealPath(dbPath);
			dbPath = path;
			System.out.println("path is set to " + path);
			items = new SAXBuilder().build(new File(path));
			populateLatestReviews();
		} catch (Exception e) {
			e.printStackTrace();
		}
		// Creating new runnable that will run in separate thread
		Runnable notifier = new Runnable() {
			@Override
			public void run() {
				boolean done = false;
				while (!done) {
					try {
						Element message = newReviews.take(); // blocking call,
																// waits until
																// there are a
																// new message
						message = getReviewElement(message);
						for (AsyncContext con : clients) {
							System.out.println("sending response to client");
							// Looping through all listening clients
							// Ensuring that content type is set to text/xml and UTF-8
							con.getResponse().setContentType("text/xml");
							con.getResponse().setCharacterEncoding("UTF-8");
							try {
								// Printing reviews to stream
								PrintWriter out = con.getResponse().getWriter();
								outputter.output(message, out);
								// Done writing, flushing and calling complete
								// to close down connection
								out.close();
								con.complete();
							} catch (IOException e) {
								e.printStackTrace();
								// Something went wrong, remove client from the
								// list of listeners
								clients.remove(con);
							}
						}
					} catch (InterruptedException e) {
						// If thread is interrupted we shut down nicely
						done = true;
					}
				}
			}
		};
		notifierThread = new Thread(notifier);
		// Start the new thread
		notifierThread.start();
	}
	
	public void destroy() {
		notifierThread.interrupt();
	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/xml");
		response.setCharacterEncoding("UTF-8");
		String item = request.getParameter("item");
		String reviews = request.getParameter("reviews");
		PrintWriter out = response.getWriter();
		
		System.out.println("item parameter: " + item);
		System.out.println("reviews parameter: " + reviews);

		if (item != null && !"".equalsIgnoreCase(item)) {
			Element elem = getElement(item);
			if (elem != null) {
				// Get specific item
				outputter.output(elem, out);
			} else {
				//TODO: error message?
			}
			out.close();
		} else if (reviews != null) {
			// get latest reviews
			if (reviews.equalsIgnoreCase("new")) {
				// First time we get reviews, get the latest
				outputter.output(getLatestReviews(), out);
				out.close();
			} else {
				System.out.println("creating async request");
				// Succeeding call, only respond when a new review arrives
				// A client wants updates
				final AsyncContext con = request.startAsync();
				// Timeout is 20 minutes
				con.setTimeout(1000 * 60 * 20);
				// We add a listener so that the client is removed whenever
				// something happens to the connection
				con.addListener(new AsyncListener() {
					@Override
					public void onComplete(AsyncEvent event) throws IOException {
						clients.remove(con);
					}

					@Override
					public void onTimeout(AsyncEvent event) throws IOException {
						clients.remove(con);
					}

					@Override
					public void onError(AsyncEvent event) throws IOException {
						clients.remove(con);
					}

					@Override
					public void onStartAsync(AsyncEvent event)
							throws IOException {
						// Do nothing here :D
					}
				});
				// Add the new asynchronous connection to the queue of listeners
				clients.add(con);
			}
		} else {
			// Send list of all items
			outputter.output(items.getRootElement(), out);
			out.close();
		}
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		// Get the item parameter
		request.setCharacterEncoding("UTF-8");
		String item = request.getParameter("item");
		String user = request.getParameter("name");
		String comment = request.getParameter("comment");
		System.out.println("got new review for " + item + " from " + user + " with text: " + comment);
		if (item != null && !"".equalsIgnoreCase(item)) {
			// We only want to do something if the parameter is set correctly
			Element elem = getElement(item);
			if (elem != null) {
				// And only when the item actually exists

				// We create new elements to store the new review
				Element review = new Element("review");
				Element user_elem = new Element("user");
				Element comment_elem = new Element("comment");
				Attribute date_attr = new Attribute("date","" + (new Date()).getTime());
				// And set the content/text in each element
				user_elem.setText(user);
				comment_elem.setText(comment);
				// Then we add the sub-elements to the review element
				review.addContent(user_elem);
				review.addContent(comment_elem);
				review.setAttribute(date_attr);
				// And add the review element as a child to the reviews element
				Element reviews = elem.getChild("reviews");
				reviews.addContent(review);
				saveDB();
				// Then we add the new review to the list of newest reviews so
				// that new clients will get an updated list when entering the
				// page
				latestReviews.add(review);
				if (latestReviews.size() > 3) {
					// And remove the oldest review if we have more than 3(we
					// only show the latest 3)
					latestReviews.poll();
				}
				// Finally we add the new review to a blocking queue so that
				// "older" clients will be notified about this new review
				newReviews.add(review);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private Element getElement(String id) {
		for (Element elem : ((List<Element>) items.getRootElement()
				.getChildren("item"))) {
			if (elem.getAttribute("iId").getValue().equalsIgnoreCase(id)) {
				return elem;
			}
		}
		return null;
	}

	private Element getLatestReviews() {
		Element reviews = new Element("reviews");
		for (Element elem : latestReviews) {
			Element clone = getReviewElement(elem);
			reviews.addContent(clone);
		}
		return reviews;
	}
	
	private Element getReviewElement(Element elem) {
		Element clone = (Element) elem.clone();
		String itemId = elem.getParentElement().getParentElement().getAttributeValue("iId");
		Element id = new Element("id");
		id.setText(itemId);
		clone.addContent(id);
		Element itemName = (Element) elem.getParentElement().getParentElement().getChild("name").clone();
		clone.addContent(itemName);
		return clone;
	}

	@SuppressWarnings("unchecked")
	private void populateLatestReviews() {
		// Create variables to be used for computation
		Element review1 = null;
		// We need a date to compare reviews
		Calendar review1Date = Calendar.getInstance();
		// Setting the date to 0 january 1st 1970 ensures that
		// any review will be newer than this date
		review1Date.setTimeInMillis(0);
		Element review2 = null;
		Calendar review2Date = Calendar.getInstance();
		review2Date.setTimeInMillis(0);
		Element review3 = null;
		Calendar review3Date = Calendar.getInstance();
		review3Date.setTimeInMillis(0);

		for (Element item : ((List<Element>) items.getRootElement()
				.getChildren("item"))) {
			System.out.println("-----review for " + item.getAttributeValue("iId") + "-----");
			for (Element review : ((List<Element>) item.getChild("reviews")
					.getChildren("review"))) {
				try {
					System.out.println("-----review-----");
					long millis = review.getAttribute("date").getLongValue();
					System.out.println("date is " + millis);
					Calendar reviewDate = Calendar.getInstance();
					reviewDate.setTimeInMillis(millis);
					if (reviewDate.after(review3Date)) {
						// This review is newer than the currently 3rd newest
						// review
						System.out.println("newer than 3rd newest which is " + review3Date.getTimeInMillis());
						review3 = review;
						review3Date = reviewDate;
						if (reviewDate.after(review2Date)) {
							// This review is also newer than the currently 2nd
							// newest review
							System.out.println("newer than 2nd newest which is " + review2Date.getTimeInMillis());
							review3 = review2;
							review3Date = review2Date;
							review2 = review;
							review2Date = reviewDate;
							if (reviewDate.after(review1Date)) {
								// This review is the newest we have seen so far
								System.out.println("newer than the newest which is " + review1Date.getTimeInMillis());
								review2 = review1;
								review2Date = review1Date;
								review1 = review;
								review1Date = reviewDate;
							}
						}
					}
				} catch (DataConversionException e) {
					e.printStackTrace();
				}
			}
		}
		// Add the three newest reviews to the queue in the correct order,
		// oldest first
		if (review3 != null) {
			// We only add if the review has been set(avoids null in the queue)
			latestReviews.add(review3);
		}
		if (review2 != null) {
			// We only add if the review has been set(avoids null in the queue)
			latestReviews.add(review2);
		}
		if (review1 != null) {
			// We only add if the review has been set(avoids null in the queue)
			latestReviews.add(review1);
		}
	}
	
	public synchronized boolean saveDB() {
		try {
			System.out.println("saving db");
			FileOutputStream fos = new FileOutputStream(dbPath);
			outputter.output(items, fos);
			return true;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return false;
	}
}